需要先了解以下主題:
此實作範例修改自五倍紅寶石技術文章
目標是讓下面的程式碼:
1     class Example
2       before_action :method_a, :method_b do
3         |n| puts "the code before #{n}"
4       end
5
6       def method_a
7         puts 'this is method a'
8       end
9
10      def method_b
11        puts 'this is method b'
12      end
13    end
14
15    instance_1 = Example.new
16    instance_1.method_a
17    instance_2 = Example.new
18    instance_2.method_b
19
可以有這樣的執行結果
1      the code before method_a
2      this is method a
3      the code before method_b
4      this is method b
首先是打開 Singleton Class 的起手式:
class 類別名稱
  class << self
    def 方法名稱
    end
  end
end
class Example
  class << self
  attr_accessor :methods, :block
    def before_action
    end
  end
end
這裡加入了 before_action 的類別方法,之後覆寫方法時,會需要取得到相關的變數,所以也ㄧ併把 attr_acessor 所提供方法都帶進去了。這代表了我們ㄧ共建立了五個類別方法:(1) :before_action, (2) :methods, (3) :methods=, (4) :block, (5) :block=
在定義 before_action() 方法會需要兩個參數(parameter):
因為在執行 before_action() 方法時,還未定義 Example 類別內的 method_a() 方法,無法在這裡執行覆寫的編碼,但可以把傳進來的引數儲存下來,而透過 attr_accessor 就可以讓其他的方法取得存下來的屬性。
class Example
  class << self
    attr_accessor :methods, :block
    def before_action(*methods, &block)
      @methods = methods
      @block = block
    end
  end
end
接著我們建立 ExampleRefinement 模組,以 refine 的方式改寫 Example 類別內要被覆寫的方法。這時候之前儲存下的的屬性就派上用場了
透過 each 方法把每一個方法名稱轉出來,以 alias_method 建立方法的新別名,並且保留原有方法的功能。接著用動態方法 define_method 重新定義方法,要執行的內容會先呼叫 block ,再用 send 呼叫原本的 method。
在最後的步驟中,透過運用動態方法、method wrappers的技巧來達到覆寫方法的目的。
module ExampleRefinement
  refine Example do
    block = Example.block
    @methods = Example.methods
    @methods.each do |method|
      newname = "new_#{method}"
      alias_method newname, method
      define_method method do
        block.call(method)
        send(newname)
      end
    end
  end
end
class Example
  class << self
    attr_accessor :methods, :block
    def before_action(*methods, &block)
      @methods = methods
      @block = block
    end
  end
end
class Example
  before_action(:method_a, :method_b){|n| p "the code before #{n}"}
  def method_a
    p 'this is method a'
  end
  def method_b
    p 'this is method b'
  end
end
module ExampleRefinement
  refine Example do
    block = Example.block
    @methods = Example.methods
    @methods.each do |method|
      # 幫原有的方法取別名
      newname = "new_#{method}"
      alias_method newname, method
      # 動態定義新方法要執行的內容
      define_method method do
        block.call(method)
        send(newname)
      end
    end
  end
end
using ExampleRefinement     # => 拿掉這一行就不會有覆寫方法的效果
instance_1 = Example.new
instance_1.method_a
instance_2 = Example.new
instance_2.method_b
執行結果會是:
the code before method_a
this is method a
the code before method_b
this is method b
目標是讓下面的程式碼:
1      class Example
2        extend BeforeAction
3
4        before :method_a, :method_b do
5          puts "the code before method"
6        end
7
8        def method_a
9          puts 'this is method a'
10       end
11
12       def method_b
13         puts 'this is method b'
14       end
15     end
16
17     instance_1 = Example.new
18     instance_1.method_a
19     instance_2 = Example.new
29     instance_2.method_b
可以有這樣的執行結果
1      the code before method
2      this is method a
3      the code before method
4      this is method b
實作的過程在原文章的最後部分,詳細的解說可以點選 這裏
以下程式碼(加上我自己的註解)是完整的答案:
module BeforeAction
  def new
    # 只有第一次執行 new 時要 execute_before
    # 因為 execute_before 內的 alias_method(),在執行第二次時會有
    # 無限迴圈的問題
    execute_before if first_time?
    
    # 再呼叫 super 才能回到原有的 new 方法
    super
  end
  def first_time?
    return false if @not_first_time
    @not_first_time = true
  end
  def before(*methods, &block)
    # 先存起來之後用
    @methods = methods
    @block = block
  end
  def execute_before
    @methods.each do |method|
      # 幫原本的 method 取一個新名字
      newname = "new_#{method}"
      alias_method newname, method
      block = @block
      # 重新定義每個 method 要做什麼
      define_method method do
        # 執行 block
        block.call
        
        # 原本的 method
        send(newname)
      end
    end
  end
end
class Example
  extend BeforeAction
  before :method_a, :method_b do
    puts 'the code before method'
  end
  def method_a
    puts 'this is method a'
  end
  def method_b
    puts 'this is method b'
  end
end
instance_1 = Example.new
instance_1.method_a
instance_2 = Example.new
instance_2.method_b
Medium 文章連結:https://kevin0117.medium.com/
本文同步發布於 Kevin's Blog: https://chienhao.tw/
備註:之後文章修改更新,以個人部落格為主